<template>
  <view v-if="!disabled" class="tn-image-upload-class tn-image-upload">
    <block v-if="showUploadList">
      <view
        v-for="(item, index) in lists"
        :key="index"
        class="tn-image-upload__item tn-image-upload__item-preview"
        :style="{
          width: $tn.string.getLengthUnitValue(width),
          height: $tn.string.getLengthUnitValue(height)
        }"
      >
        <!-- 删除按钮 -->
        <view
          v-if="deleteable"
          class="tn-image-upload__item-preview__delete"
          @tap.stop="deleteItem(index)"
          :style="{
            borderTopColor: deleteBackgroundColor
          }"
        >
          <view
            class="tn-image-upload__item-preview__delete--icon"
            :class="[`tn-icon-${deleteIcon}`]"
            :style="{
              color: deleteColor
            }"
          ></view>
        </view>
        <!-- 进度条 -->
        <tn-line-progress
          v-if="showProgress && item.progress > 0 && !item.error"
          class="tn-image-upload__item-preview__progress"
          :percent="item.progress"
          :showPercent="false"
          :round="false"
          :height="8"
        ></tn-line-progress>
        <!-- 重试按钮 -->
        <view v-if="item.error" class="tn-image-upload__item-preview__error-btn" @tap.stop="retry(index)">点击重试</view>
        <!-- 图片信息 -->
        <image
          class="tn-image-upload__item-preview__image"
          :src="item.url || item.path"
          :mode="imageMode"
          @tap.stop="doPreviewImage(item.url || item.path, index)"
        ></image>
      </view>
    </block>
    <!-- <view v-if="$slots.file || $slots.$file" style="width: 100%;">
      
    </view> -->
    <!-- 自定义图片展示列表 -->
    <slot name="file" :file="lists"></slot>
    
    <!-- 添加按钮 -->
    <view v-if="maxCount > lists.length" class="tn-image-upload__add" :class="{'tn-image-upload__add--custom': customBtn}" @tap="selectFile">
      <!-- 添加按钮 -->
      <view
        v-if="!customBtn"
        class="tn-image-upload__item tn-image-upload__item-add"
        hover-class="tn-hover-class"
        hover-stay-time="150"
        :style="{
          width: $tn.string.getLengthUnitValue(width),
          height: $tn.string.getLengthUnitValue(height)
        }"
      >
        <view class="tn-image-upload__item-add--icon tn-icon-add"></view>
        <view class="tn-image-upload__item-add__tips">{{ uploadText }}</view>
      </view>
      <!-- 自定义添加按钮 -->
      <view>
        <slot name="addBtn"></slot>
      </view>
    </view>
  </view>
</template>

<script>
  export default {
    name: 'tn-image-upload',
    props: {
      // 已上传的文件列表
      fileList: {
        type: Array,
        default() {
          return []
        }
      },
      // 上传图片地址
      action: {
        type: String,
        default: ''
      },
      // 上传文件的字段名称
      name: {
        type: String,
        default: 'file'
      },
      // 头部信息
      header: {
        type: Object,
        default() {
          return {}
        }
      },
      // 携带的参数
      formData: {
        type: Object,
        default() {
          return {}
        }
      },
      // 是否禁用
      disabled: {
        type: Boolean,
        default: false
      },
      // 是否自动上传
      autoUpload: {
        type: Boolean,
        default: true
      },
      // 最大上传数量
      maxCount: {
        type: Number,
        default: 9
      },
      // 是否显示组件自带的图片预览
      showUploadList: {
        type: Boolean,
        default: true
      },
      // 预览上传图片的裁剪模式
      imageMode: {
        type: String,
        default: 'aspectFill'
      },
      // 点击图片是否全屏预览
      previewFullImage: {
        type: Boolean,
        default: true
      },
      // 是否显示进度条
      showProgress: {
        type: Boolean,
        default: true
      },
      // 是否显示删除按钮
      deleteable: {
        type: Boolean,
        default: true
      },
      // 删除按钮图标
      deleteIcon: {
        type: String,
        default: 'close'
      },
      // 删除按钮的背景颜色
      deleteBackgroundColor: {
        type: String,
        default: ''
      },
      // 删除按钮的颜色
      deleteColor: {
        type: String,
        default: ''
      },
      // 上传区域提示文字
      uploadText: {
        type: String,
        default: '选择图片'
      },
      // 显示toast提示
      showTips: {
        type: Boolean,
        default: true
      },
      // 自定义选择图标按钮
      customBtn: {
        type: Boolean,
        default: false
      },
      // 预览图片和选择图片区域的宽度
      width: {
        type: Number,
        default: 200
      },
      // 预览图片和选择图片区域的高度
      height: {
        type: Number,
        default: 200
      },
      // 选择图片的尺寸
      // 参考上传文档 https://uniapp.dcloud.io/api/media/image
      sizeType: {
        type: Array,
        default() {
          return ['original', 'compressed']
        }
      },
      // 图片来源
      sourceType: {
        type: Array,
        default() {
          return ['album', 'camera']
        }
      },
      // 是否可以多选
      multiple: {
        type: Boolean,
        default: true
      },
      // 文件大小(byte)
      maxSize: {
        type: Number,
        default: 10 * 1024 * 1024
      },
      // 允许上传的类型
      limitType: {
        type: Array,
        default() {
          return ['png','jpg','jpeg','webp','gif','image']
        }
      },
      // 是否自定转换为json
      toJson: {
        type: Boolean,
        default: true
      },
      // 上传前钩子函数，每个文件上传前都会执行
      beforeUpload: {
        type: Function,
        default: null
      },
      // 删除文件前钩子函数
      beforeRemove: {
        type: Function,
        default: null
      },
      index: {
        type: [Number, String],
        default: ''
      }
    },
    computed: {
      
    },
    data() {
      return {
        lists: [],
        uploading: false
      }
    },
    watch: {
      fileList: {
        handler(val) {
          val.map(value => {
            // 首先检查内部是否已经添加过这张图片，因为外部绑定了一个对象给fileList的话(对象引用)，进行修改外部fileList时，
            // 会触发watch，导致重新把原来的图片再次添加到this.lists
            // 数组的some方法意思是，只要数组元素有任意一个元素条件符合，就返回true，而另一个数组的every方法的意思是数组所有元素都符合条件才返回true
            let tmp = this.lists.some(listVal => {
              return listVal.url === value.url
            })
            // 如果内部没有这张图片，则添加到内部
            !tmp && this.lists.push({ url: value.url, error: false, progress: 100 })
          })
        },
        immediate: true
      },
      lists(val) {
        this.$emit('on-list-change', val, this.index)
      }
    },
    methods: {
      // 清除列表
      clear() {
        this.lists = []
      },
      // 重新上传队列中上传失败所有文件
      reUpload() {
        this.uploadFile()
      },
      // 选择图片
      selectFile() {
        if (this.disabled) return
        const {
          name = '',
          maxCount,
          multiple,
          maxSize,
          sizeType,
          lists,
          camera,
          compressed,
          sourceType
        } = this
        let chooseFile = null
        const newMaxCount = maxCount - lists.length
        // 只选择图片的时候使用 chooseImage 来实现
        chooseFile = new Promise((resolve, reject) => {
          uni.chooseImage({
            count: multiple ? (newMaxCount > 9 ? 9 : newMaxCount) : 1,
            sourceType,
            sizeType,
            success: resolve,
            fail: reject
          })
        })
        chooseFile.then(res => {
          let file = null
          let listOldLength = lists.length
          res.tempFiles.map((val, index) => {
            if (!this.checkFileExt(val)) return
            
            // 是否超出最大限制数量
            if (!multiple && index >= 1) return
            if (val.size > maxSize) {
              this.$emit('on-oversize', val, lists, this.index)
              this.showToast('超出可允许文件大小')
            } else {
              if (maxCount <= lists.length) {
                this.$emit('on-exceed', val, lists, this.index)
                this.showToast('超出最大允许的文件数')
                return
              }
              lists.push({
                url: val.path,
                progress: 0,
                error: false,
                file: val
              })
            }
          })
          this.$emit('on-choose-complete', this.lists, this.index)
          if (this.autoUpload) this.uploadFile(listOldLength)
        }).catch(err => {
          this.$emit('on-choose-fail', err)
        })
      },
      // 提示用户信息
      showToast(message, force = false) {
        if (this.showTips || force) {
          this.$tn.message.toast(message)
        }
      },
      // 手动上传，通过ref进行调用
      upload() {
        this.uploadFile()
      },
      // 对失败图片进行再次上传
      retry(index) {
        this.lists[index].progress = 0
        this.lists[index].error = false
        this.lists[index].response = null
        this.$tn.message.loading('重新上传')
        this.uploadFile(index)
      },
      // 上传文件
      async uploadFile(index = 0) {
        if (this.disabled) return
        if (this.uploading) return
        // 全部上传完成
        if (index >= this.lists.length) {
          this.$emit('on-uploaded', this.lists, this.index)
          return
        }
        // 检查是否已经全部上传或者上传中
        if (this.lists[index].progress === 100) {
          this.lists[index].uploadTask = null
          if (this.autoUpload) this.uploadFile(index + 1)
          return
        }
        // 执行before-upload钩子
        if (this.beforeUpload && typeof(this.beforeUpload) === 'function') {
          // 在微信，支付宝等环境(H5正常)，会导致父组件定义的函数体中的this变成子组件的this
          // 通过bind()方法，绑定父组件的this，让this的this为父组件的上下文
          // 因为upload组件可能会被嵌套在其他组件内，比如tn-form，这时this.$parent其实为tn-form的this，
          // 非页面的this，所以这里需要往上历遍，一直寻找到最顶端的$parent，这里用了this.$u.$parent.call(this)
          let beforeResponse = this.beforeUpload.bind(this.$tn.$parent.call(this))(index, this.lists)
          // 判断是否返回了Promise
          if (!!beforeResponse && typeof beforeResponse.then === 'function') {
            await beforeResponse.then(res => {
              // promise返回成功，不进行操作继续
            }).catch(err => {
              // 进入catch回调的话，继续下一张
              return this.uploadFile(index + 1)
            })
          } else if (beforeResponse === false) {
            // 如果返回flase，继续下一张图片上传
            return this.uploadFile(index + 1)
          } else {
            // 为true的情况，不进行操作
          }
        }
        // 检查上传地址
        if (!this.action) {
          this.showToast('请配置上传地址', true)
          return
        }
        this.lists[index].error = false
        this.uploading = true
        // 创建上传对象
        const task = uni.uploadFile({
          url: this.action,
          filePath: this.lists[index].url,
          name: this.name,
          formData: this.formData,
          header: this.header,
          success: res => {
            // 判断啊是否为json字符串，将其转换为json格式
            let data = this.toJson && this.$tn.test.jsonString(res.data) ? JSON.parse(res.data) : res.data
            if (![200, 201, 204].includes(res.statusCode)) {
              this.uploadError(index, data)
            } else {
              this.lists[index].response = data
              this.lists[index].progress = 100
              this.lists[index].error = false
              this.$emit('on-success', data, index, this.lists, this.index)
            }
          },
          fail: err => {
            this.uploadError(index, err)
          },
          complete: res => {
            this.$tn.message.closeLoading()
            this.uploading = false
            this.uploadFile(index + 1)
            this.$emit('on-change', res, index, this.lists, this.index)
          }
        })
        this.lists[index].uploadTask = task
        task.onProgressUpdate(res => {
          if (res.progress > 0) {
            this.lists[index].progress = res.progress
            this.$emit('on-progress', res, index, this.lists, this.index)
          }
        })
      },
      // 上传失败
      uploadError(index, err) {
        this.lists[index].progress = 0
        this.lists[index].error = true
        this.lists[index].response = null
        this.showToast('上传失败，请重试')
        this.$emit('on-error', err, index, this.lists, this.index)
      },
      // 删除一个图片
      deleteItem(index) {
        if (!this.deleteable) return
        this.$tn.message.modal(
          '提示',
          '您确定要删除吗？',
          async () => {
            // 先检查是否有定义before-remove移除前钩子
            // 执行before-remove钩子
            if (this.beforeRemove && typeof(this.beforeRemove) === 'function') {
              let beforeResponse = this.beforeRemove.bind(this.$tn.$parent.call(this))(index, this.lists)
              // 判断是否返回promise 
              if (!!beforeResponse && typeof beforeResponse.then === 'function') {
                await beforeResponse.then(res => {
                  // promise返回成功不进行操作
                  this.handlerDeleteItem(index)
                }).catch(err => {
                  this.showToast('删除操作被中断')
                })
              } else if (beforeResponse === false) {
                this.showToast('删除操作被中断')
              } else {
                this.handlerDeleteItem(index)
              }
            } else {
              this.handlerDeleteItem(index)
            }
          }, true)
      },
      // 移除文件操作
      handlerDeleteItem(index) {
        // 如果文件正在上传中，终止上传任务
        if (this.lists[index].progress < 100 && this.lists[index].progress > 0) {
          typeof this.lists[index].uploadTask !== 'undefined' && this.lists[index].uploadTask.abort()
        }
        this.lists.splice(index, 1)
        this.$forceUpdate()
        this.$emit('on-remove', index, this.lists, this.index)
        this.showToast('删除成功')
      },
      // 移除文件，通过ref手动形式进行调用
      remove(index) {
        if (!this.deleteable) return
        // 判断索引合法
        if (index >= 0 && index < this.lists.length) {
          this.lists.splice(index, 1)
        }
      },
      // 预览图片
      doPreviewImage(url, index) {
        if (!this.previewFullImage) return
        const images = this.lists.map(item => item.url || item.path)
        uni.previewImage({
          urls: images,
          current: url,
          success: () => {
            this.$emit('on-preview', url, this.lists, this.index)
          },
          fail: () => {
            this.showToast('预览图片失败')
          }
        })
      },
      // 检查文件后缀是否合法
      checkFileExt(file) {
        // 是否为合法后缀
        let noArrowExt = false
        // 后缀名
        let fileExt = ''
        const reg = /.+\./
        
        // #ifdef H5
        fileExt = file.name.replace(reg, '').toLowerCase()
        // #endif
        // #ifndef H5
        fileExt = file.path.replace(reg, '').toLowerCase()
        // #endif
        noArrowExt = this.limitType.some(ext => {
          return ext.toLowerCase() === fileExt
        })
        if (!noArrowExt) this.showToast(`不支持${fileExt}格式的文件`)
        return noArrowExt
      }
    }
  }
</script>

<style lang="scss" scoped>
  
  .tn-image-upload {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    
    &__item {
      /* #ifndef APP-NVUE */
      display: flex;
      /* #endif */
      align-items: center;
      justify-content: center;
      width: 200rpx;
      height: 200rpx;
      overflow: hidden;
      margin: 12rpx;
      margin-left: 0;
      background-color: $tn-font-holder-color;
      position: relative;
      border-radius: 10rpx;
      
      &-preview {
        border: 1rpx solid $tn-border-solid-color;
        
        &__delete {
          display: flex;
          align-items: center;
          justify-content: center;
          position: absolute;
          top: 0;
          right: 0;
          z-index: 10;
          border-top: 60rpx solid;
          border-left: 60rpx solid transparent;
          border-top-color: $tn-color-red;
          width: 0rpx;
          height: 0rpx;
          
          &--icon {
            position: absolute;
            top: -50rpx;
            right: 6rpx;
            color: #FFFFFF;
            font-size: 24rpx;
            line-height: 1;
          }
        }
        
        &__progress {
          position: absolute;
          width: auto;
          bottom: 0rpx;
          left: 0rpx;
          right: 0rpx;
          z-index: 9;
          /* #ifdef MP-WEIXIN */
          display: inline-flex;
          /* #endif */
        }
        
        &__error-btn {
          position: absolute;
          bottom: 0;
          left: 0;
          right: 0;
          background-color: $tn-color-red;
          color: #FFFFFF;
          font-size: 20rpx;
          padding: 8rpx 0;
          text-align: center;
          z-index: 9;
          line-height: 1;
        }
        
        &__image {
          display: block;
          width: 100%;
          height: 100%;
          border-radius: 10rpx;
        }
      }
      
      &-add {
        flex-direction: column;
        color: $tn-content-color;
        font-size: 26rpx;
        
        &--icon {
          font-size: 40rpx;
        }
        
        &__tips {
          margin-top: 20rpx;
          line-height: 40rpx;
        }
      }
    }
    
    &__add {
      width: auto;
      display: inline-block;
      
      &--custom {
        width: 100%;
      }
    }
  }
</style>
